import { useState, useEffect, useRef, useCallback, memo } from 'react'

import { Toast } from 'antd-mobile'

import axios from 'axios'

import { getRandomString } from '@/utils'

import store from '@/utils/storageManage'

import './styles.less'

// 编码
function objectToStr(obj: { [key: string]: any }) {
  obj = { ...obj, msg: encodeURIComponent(obj.msg) }
  return JSON.stringify(obj)
}

// 解码
function stringToObj(str: string) {
  const obj = JSON.parse(str)
  return { ...obj, msg: decodeURIComponent(obj.msg) }
}

// 用户类型
const SING_TYPE = 'service'

// timeout的类型
let timer: NodeJS.Timeout

// 消息类型
interface IMsg {
  type?: string
  from?: string
  msg?: string
  name?: string
}

/**
 * 组件
 * @returns
 */
function Service() {
  const [value, setValue] = useState('') // 输入框内容
  const [msg, setMsg] = useState<IMsg[]>([]) // 消息列表
  const [connectStatus, setConnectState] = useState(false) // 连接状态
  const uploadInput = useRef<HTMLInputElement>(null)

  // 建立一个webSocket连接，括号里的参数表示当前需要连接的服务器
  // 注意：服务器的前缀是ws;表示当前服务器没有经过加密，如果wss就表示服务器加密了
  const ws = useRef<WebSocket>()

  /**
   * 创建一个新的连接
   */
  const createSocket = useCallback(() => {
    let datas: any[] = []
    // 连接服务器
    const socket = (ws.current = new WebSocket('ws://10.161.36.103:2929'))

    /**
     * 心跳检测
     */
    function heartCheck() {
      clearTimeout(timer)
      // 开始心跳检测的计时
      // 为了保证连接的有效性，3分钟没有接收到服务器的任何消息，尝试发送一条消息检查连接状态
      timer = setTimeout(() => {
        socket.send(
          objectToStr({
            type: 'heart',
          })
        )
        // 消息发送完成后，修改计时器，等待30秒如果没有返回，中断当前连接并重新建立连接
        timer = setTimeout(() => {
          // code: 代码, reason: 原因
          socket.close(4001, '连接失活，重新建立连接')
          // 重新建立连接
          // 转成异步，等待当前同步任务执行完成再创建新的连接，避免一些问题
          setTimeout(() => {
            createSocket()
          })
        }, 30000)
      }, 3 * 60 * 1000)
    }

    /**
     * socket连接成功的回调
     * @param evt
     */
    socket.onopen = evt => {
      // 标记连接成功
      setConnectState(true)
      // 检查用户以前是否登陆过
      const userId = store.get('socketName')

      // 使用send方法，向服务器发送消息
      socket.send(
        objectToStr({
          userId,
          // 类型为注册
          type: 'sign',
          // 用户身份
          from: SING_TYPE,
        })
      )
      // 开始检测心跳
      heartCheck()
    }

    /**
     * 接收到服务器消息的回调
     * @param evt
     */
    socket.onmessage = evt => {
      // 消息类型 消息 用户名 用户长期保存的标识 消息来源
      const { msg, name, from, type } = stringToObj(evt.data)

      if (type === 'sign') {
        return store.set('socketName', name)
      }

      // 判断是否是心跳检测或其它原因导致服务器关闭连接了
      if (type === 'close') {
        return socket.close(4209, '服务器主动关闭连接')
      }

      // 心跳检测不需要展示给用户
      if (type === 'heart') {
        return heartCheck()
      }

      // 收到其它消息也可以证明当前连接是正常的，重新开始心跳计时
      heartCheck()

      // 写入消息队列
      datas = [
        ...datas,
        {
          msg,
          name,
          from,
          // 将消息加入到队列时生成一个随机数作为它的key
          key: getRandomString(),
        },
      ]
      setMsg(datas)
    }

    /**
     * 服务器被关闭
     * @param evt
     */
    socket.onclose = evt => {
      // 服务器主动关闭连接
      if (evt.code === 1006) {
        console.log('服务器断开连接，正常处理是写一个延时重连的方案')
        socket && socket.close()
        return setConnectState(false)
      }
      console.log('连接关闭了，我也不知道咋回事', evt.code)
    }

    /**
     * 连接异常
     * @param evt
     */
    socket.onerror = evt => {
      console.log('连接错误', evt)
      setConnectState(false)
      socket && socket.close()
    }
  }, [])

  /**
   * 组件加载时执行
   */
  useEffect(() => {
    createSocket()
    return () => {
      // 清除计时，关闭连接
      ws.current && ws.current.close(4009, '组件卸载或退出浏览器')
      clearTimeout(timer)
    }
  }, [])

  /**
   * 发送消息
   */
  const sendFun = useCallback(() => {
    // 将当前的输入框里的内容推送到服务器
    if (ws.current && connectStatus) {
      ws.current.send(
        objectToStr({
          type: 'msg',
          msg: value,
          from: SING_TYPE,
        })
      )
    } else {
      createSocket()
    }

    // value发生变化后，更新函数
  }, [value])

  /**
   * 图片上传
   */
  const uploadImage = useCallback(() => {
    if (!uploadInput.current || !uploadInput.current.files) {
      return
    }

    const fileInfo = uploadInput.current.files[0]

    // 非图片文件筛选
    if (fileInfo.type !== 'image/png' && fileInfo.type !== 'image/jpeg') {
      Toast.show('只允许发送图片文件')
      return
    }

    // 图片大小不能超过2M
    if (fileInfo.size / 1024 > 2048) {
      Toast.show('图片大小不能超过2MB')
      return
    }
    // 创建一个formData对象
    let param = new FormData()

    //通过append向formData对象添加数据(这个看各个服务器要求)
    param.append('filename', 'pics')
    param.append('pics', fileInfo)

    // FormData私有类对象，访问不到，可以通过get判断值是否传进去
    console.log(param.get('pics'), '看看图片是否成功写入到formData中')

    // 修改请求头
    const config = {
      // headers，也是看各服务器自己的要求
      headers: {
        // 这个必须有，表示这是在上传文件，没有这个就成了平常的数据提交了
        'Content-Type': 'multipart/form-data',
        // token，这个
        'Authori-zation': `Bearer ${store.get('token')}`,
      },
    }

    // 提交数据
    axios.post('http://shop.fenotes.com/api/upload/image', param, config).then(response => {
      console.log(response, '图片上传成功，拼接成图片标签发往服务器')
      // 将当前的输入框里的内容推送到服务器
      if (ws.current && connectStatus) {
        ws.current.send(
          objectToStr({
            type: 'msg',
            msg: `<img src=${response.data.data.url} alt="" />`,
            from: SING_TYPE,
          })
        )
      } else {
        createSocket()
      }
    })
  }, [])

  return (
    <div className="docwrap service">
      <div className={`chat-room ${SING_TYPE}`}>
        {/* 把消息队列里的消息展示出来 */}
        {msg.map((item: any) => (
          <Msg key={item.key} data={item} />
        ))}
      </div>
      <div className="chat-control">
        <input
          className="send-input"
          placeholder="请输入内容"
          value={value}
          onChange={(e: any) => {
            setValue(e.target.value)
          }}
        />
        <button onClick={sendFun} className="send-btn">
          发送
        </button>
        <div className="upload">
          <input onChange={uploadImage} type="file" className="uploader-input" ref={uploadInput} />
          上传
        </div>
      </div>
    </div>
  )
}

export default Service

const Msg = memo(function ({ data }: any) {
  /**
   * 消息处理
   */
  const handlerMsg = useCallback((msg: string) => {
    console.log(msg, '处理消息')
    // 检查消息中是否有标签;这里没有考虑XSS攻击等，只是简单地将消息中的内容直接展示出来
    if (/<[^>]+>/g.test(msg)) {
      return <div className="msg" dangerouslySetInnerHTML={{ __html: msg }}></div>
    }
    return <div className="msg">{msg}</div>
  }, [])
  return (
    <div className={`msg-wrap ${data.from}`}>
      <div className="from">{data.name}</div>
      {handlerMsg(data.msg)}
    </div>
  )
})
